"""
===============================
@author     : yaoruiqi

@Time       : 2023/3/3 14:54

@version    : V

@introduce  :

@change     : 
===============================
"""
import asyncio
from datetime import datetime
from json import loads, dumps
from re import search
from time import mktime, strptime, localtime, strftime
from uuid import uuid4

from apscheduler.schedulers.background import BackgroundScheduler
from asgiref.sync import sync_to_async
from django.conf import settings
from django.core.cache import cache
from django.db import connections
from loguru import logger

from api.baidu_api import package_baidu_api
from api.public_methods import get_access_token, get_developer_open_id
from api.wechat_api import access_token_manager, template_message, get_subscription_all_user, template_management, \
    get_subscription_user_info
from app_management.models import CommemorationDate
from app_subscription_account.models import SubscriptionUser
from beachburg_joint_plan.settings import SERVICE_BASE_INFO


def clean_mysql_connect():
    try:
        [conn.close_if_unusable_or_obsolete() for conn in connections.all()]
    except Exception as e:
        logger.exception(f'清理过期mysql连接失败，{e}')


class RunAlone:

    def __init__(self, name: str, release_time: int = 60):
        self.name = name
        self.release_time = release_time

    def __call__(self, fun):
        name = self.name

        def run_alone(*args, **kwargs):
            uid = str(uuid4())
            if cache.set(f'aps_task_lock:{name}', uid, self.release_time, nx=True):
                logger.debug(f'本进程执行 {fun.__doc__}')
                fun_res = fun(*args, **kwargs)
                return fun_res
            else:
                return True

        return run_alone


class FlushAccessToken:

    @staticmethod
    async def manager():
        redis_key_mapping = {}
        flush_token_async_task_list = []
        failed_tips_async_task_list = []

        for service_name, service_info in SERVICE_BASE_INFO.items():
            app_id = service_info['app_id']
            app_secret = service_info['app_secret']
            redis_key_mapping[service_name] = f'access_token:{service_name}'
            flush_token_async_task_list.append(
                access_token_manager.async_flush_access_token(app_id, app_secret, service_name)
            )

        for task_result in await asyncio.gather(*flush_token_async_task_list):
            token_result, service_name = task_result
            if token_result:
                cache.set(redis_key_mapping[service_name], token_result, 60 * 90)
                logger.success(f'access_token刷新成功  service:{service_name}  token:{token_result}')
            else:
                logger.error(f'access_token刷新失败  service:{service_name}')
                for developer_open_id in get_developer_open_id():
                    failed_tips_async_task_list.append(template_message.async_send_tempalte_massage(
                        access_token=get_access_token(service_name='subscription_tips'),
                        user_open_id=developer_open_id,
                        template_name='服务异常通知',
                        time=strftime("%Y-%m-%d %H:%M:%S", localtime()),
                        service_name=service_name,
                        feedback='token刷新失败'
                    ))

        failed_tips_async_task_list and [_ for _ in await asyncio.gather(*failed_tips_async_task_list)]

    @RunAlone(name='flush_access_token')
    def start(self):
        """ 刷新access_token任务 """
        asyncio.run(self.manager())


class DailyPush:

    @staticmethod
    async def get_weather_data(adcode_set: set, today: str):
        """
        获取推送信息
        @param adcode_set: 区域代码集合
        @param today: 当天日期yyyy-mm-dd
        @return:
        """
        weather_dict = {}

        get_weather_task_list = []
        for adcode in adcode_set:
            cahce_key = f'weather_info_json:{adcode}'
            if cache_data := cache.get(cahce_key):
                weather = loads(cache_data)
                now_info = weather['now']
                today_info = weather[today]
                location_info = weather['location']
                weather_dict[adcode] = {
                    'location': f"{location_info['city']}{location_info['name']}",  # 地理位置
                    'now_temp': now_info['temp'],  # 当前温度
                    'now_weather': now_info['text'],  # 天气状况
                    'now_wind': f"{now_info['wind_dir']} {now_info['wind_class']}",  # 风
                    'today_text_day': today_info['text_day'],  # 白天天气现象
                    'today_text_night': today_info['text_night'],  # 晚上天气现象
                    'today_high': f"{today_info['high']}℃",  # 最高温度
                    'today_low': f"{today_info['low']}℃"  # 最低温度
                }
            else:
                get_weather_task_list.append(package_baidu_api.async_get_weather(adcode))

        for task_result in await asyncio.gather(*get_weather_task_list):
            assert task_result, '行政区号获取天气信息失败'
            adcode, weather = task_result
            now_info = weather['now']
            today_info = weather[today]
            location_info = weather['location']
            data = {
                'location': f"{location_info['city']}{location_info['name']}",  # 地理位置
                'now_temp': now_info['temp'],  # 当前温度
                'now_weather': now_info['text'],  # 天气状况
                'now_wind': f"{now_info['wind_dir']} {now_info['wind_class']}",  # 风
                'today_text_day': today_info['text_day'],  # 白天天气现象
                'today_text_night': today_info['text_night'],  # 晚上天气现象
                'today_high': f"{today_info['high']}℃",  # 最高温度
                'today_low': f"{today_info['low']}℃"  # 最低温度
            }
            weather_dict[adcode] = data
        assert weather_dict, '获取天气信息失败'
        return weather_dict

    @staticmethod
    def get_date_data(today_date):
        """ 获取纪念日数据 """
        cache_key = f'special_date_json'
        if cache_data := cache.get(cache_key):
            if (cache_data := loads(cache_data)).get('today_date') == today_date:
                return cache_data
        else:
            sql = f"""
            (
                SELECT
                    `id`,
                    `name`,
                    CASE
                        WHEN DATE_FORMAT(date, '%%m-%%d') >= DATE_FORMAT(CURDATE(), '%%m-%%d') THEN DATEDIFF(
                            DATE_FORMAT(date, CONCAT(YEAR(CURDATE()), '-%%m-%%d')), CURDATE()
                        )
                        ELSE DATEDIFF(
                            DATE_FORMAT(date, CONCAT(YEAR(CURDATE()) + 1, '-%%m-%%d')), CURDATE()
                        )
                    END AS next_anniversary_remaining_days,
                    DATEDIFF(CURDATE(), date) + 1 AS days_passed
                FROM
                    special_date
                ORDER BY
                    next_anniversary_remaining_days
                LIMIT 1
            ) UNION (
                SELECT
                    `id`,
                    `name`,
                    CASE
                        WHEN DATE_FORMAT(date, '%%m-%%d') >= DATE_FORMAT(CURDATE(), '%%m-%%d') THEN DATEDIFF(
                            DATE_FORMAT(date, CONCAT(YEAR(CURDATE()), '-%%m-%%d')), CURDATE()
                        )
                        ELSE DATEDIFF(
                            DATE_FORMAT(date, CONCAT(YEAR(CURDATE()) + 1, '-%%m-%%d')), CURDATE()
                        )
                    END AS next_anniversary_remaining_days,
                    DATEDIFF(CURDATE(), date) + 1 AS days_passed
                FROM
                    special_date
                WHERE
                    `name` = '我们在一起的日子'
            )
            """
            date_query = CommemorationDate.objects.raw(sql)
            assert date_query, '获取纪念日信息失败'
            love_date_delta = None  # 在一起的天数
            next_anniversary = None  # 下一个纪念日事件
            next_anniversary_remaining_days = None  # 下一个纪念日剩余天数
            for i in date_query:
                if i.id == 1:
                    love_date_delta = i.days_passed
                if next_anniversary is None or i.next_anniversary_remaining_days < next_anniversary_remaining_days:
                    next_anniversary = i.name
                    next_anniversary_remaining_days = i.next_anniversary_remaining_days
            assert all([love_date_delta, next_anniversary, next_anniversary_remaining_days]), '获取纪念日信息失败'
            cache_data = {
                'today_date': today_date,
                'love_date_delta': love_date_delta,
                'next_anniversary': next_anniversary,
                'next_anniversary_remaining_days': next_anniversary_remaining_days
            }
            cache.set(cache_key, dumps(cache_data), timeout=60 * 60)
            return cache_data

    @staticmethod
    def get_all_user() -> dict:
        """
        获取推送公众号所有订阅用户open_id和用户位置，没有获取到位置的默认使用北京市东城区
        """
        # todo: 加入缓存机制
        clean_mysql_connect()
        return {
            i['openid']: adcode if (adcode := i.get('location_adcode')) else '110101'
            for i in SubscriptionUser.objects.filter(subscribe=1).values('openid', 'location_adcode')
        }

    async def manager(self, all_user_info: dict = None):
        """
        给用户发送每日推送
        @param all_user_info: {用户openid: 地理位置adcode}
        @return:
        """
        try:
            if all_user_info is None:
                all_user_info = await sync_to_async(self.get_all_user)()
            assert all_user_info, '查询公众号所有订阅用户openid失败'

            today_date = str(datetime.now().date())

            weather_dict = await self.get_weather_data(adcode_set=set(all_user_info.values()), today=today_date)

            specail_date_dict = await sync_to_async(self.get_date_data)(today_date)
            today_date, love_date_delta, next_anniversary, next_anniversary_remaining_days = specail_date_dict.values()

            push_template_async_task_list = []
            for openid, adcode in all_user_info.items():
                weather_info = weather_dict[adcode]
                push_template_async_task_list.append(
                    template_message.async_send_tempalte_massage(
                        access_token=get_access_token(service_name='subscription_tips'),
                        user_open_id=openid,
                        template_name='每日推送',
                        date=today_date,
                        love_day=love_date_delta,
                        event_name=next_anniversary,
                        event_day=next_anniversary_remaining_days,
                        location=weather_info['location'],
                        now_weather=weather_info['now_weather'],
                        now_temp=weather_info['now_temp'],
                        now_wind=weather_info['now_wind'],
                        today_text_day=weather_info['today_text_day'],
                        today_text_night=weather_info['today_text_night'],
                        today_high=weather_info['today_high'],
                        today_low=weather_info['today_low'],
                    )
                )

            for result in await asyncio.gather(*push_template_async_task_list):
                if not result:
                    for developer_open_id in get_developer_open_id():
                        template_message.send_template_message(
                            access_token=get_access_token(service_name='subscription_tips'),
                            user_open_id=developer_open_id,
                            template_name='服务异常通知',
                            time=strftime("%Y-%m-%d %H:%M:%S", localtime()),
                            service_name='每日早安推送',
                            feedback='发送推送失败，请尽快查看'
                        )
                    return False
            return True
        except AssertionError as e:
            logger.error(f'{e}')
            return False
        except Exception as e:
            logger.exception(f'每日推送发生异常，{e}')
            return False

    @RunAlone(name='daily_push')
    def start(self):
        """ 每日推送任务 """
        asyncio.run(self.manager())


class GetTipsSubscriptionUserInfo:

    @staticmethod
    def search_mysql_user_info():
        clean_mysql_connect()
        subscription_objs = SubscriptionUser.objects.all().values('openid')
        had_subscription_openid_count = subscription_objs.count()
        had_subscription_openid_set = {i['openid'] for i in subscription_objs}
        return had_subscription_openid_count, had_subscription_openid_set

    async def manager(self):
        try:
            access_token = get_access_token(service_name='subscription_tips')
            all_user = await sync_to_async(get_subscription_all_user.get_all_user)(access_token)
            assert all_user, '补全公众号用户信息失败'
            total_user_count = all_user['total']
            had_subscription_openid_count, had_subscription_openid_set = await sync_to_async(
                self.search_mysql_user_info)()
            if total_user_count > had_subscription_openid_count:
                async_task_list = []
                create_data = []
                had_subscription_openid = {openid for openid in had_subscription_openid_set}
                total_user_openid_set = set(all_user['data']['openid'])
                lack_openid_set = total_user_openid_set.difference(had_subscription_openid)
                for openid in lack_openid_set:
                    async_task_list.append(get_subscription_user_info.async_get_user_info(access_token, openid))
                for task_result in await asyncio.gather(*async_task_list):
                    if task_result:
                        create_data.append(SubscriptionUser(
                            openid=task_result['openid'],
                            subscribe=task_result['subscribe'],
                            subscribe_time=task_result['subscribe_time'],
                            remark=task_result['remark'],
                            groupid=task_result['groupid'],
                            tagid=';'.join(task_result['tagid_list']),
                            subscribe_scene=task_result['subscribe_scene'],
                            subscription='tips'
                        ))
                return create_data
            else:
                return None
        except AssertionError as e:
            logger.error(f'{e}')
            return False
        except Exception as e:
            logger.exception(f'补全公众号用户信息失败，{e}')
            return False

    @RunAlone(name='update_tips_subscription_user_info')
    def start(self):
        """ 补全公众号用户信息 """
        create_data = asyncio.run(self.manager())
        if create_data:
            SubscriptionUser.objects.bulk_create(create_data)
            logger.success('公众号用户信息补全完成')
        else:
            logger.success('公众号用户信息无需补充')


class CleanLogs:

    @staticmethod
    @RunAlone(name='clean_logs')
    def start():
        """ 清理日志文件 """
        today_log_ts = int(mktime(strptime(strftime("%Y-%m-%d", localtime()), '%Y-%m-%d')))
        for log in settings.LOG_DIR.iterdir():
            if log_date_re := search(r'^(\d{4}-\d{2}-\d{2})\.log$', log.name):
                log_ts = int(mktime(strptime(log_date_re[1], '%Y-%m-%d')))
                if (today_log_ts - log_ts) > 24 * 60 * 60 * 3:
                    try:
                        log.unlink(missing_ok=True)
                    except Exception as e:
                        logger.exception(f'青梨日志失败，{e}')


class UpdateTemplateInfo:

    @staticmethod
    @RunAlone(name='update_template_info')
    def start():
        """ 更新模版信息 """
        template_management.sync_template_info(access_token=get_access_token(service_name='subscription_tips'))


def run_aps_task():
    # 启动便运行
    FlushAccessToken().start()

    # 定时任务
    scheduler = BackgroundScheduler()
    scheduler.add_job(func=FlushAccessToken().start, trigger='interval', hours=1.5, id='flush_access_token')
    scheduler.add_job(func=DailyPush().start, trigger='cron', hour=7, minute=0, id='daily_push')
    scheduler.add_job(
        func=GetTipsSubscriptionUserInfo().start, trigger='cron', hour=5, minute=0,
        id='update_tips_subscription_user_info'
    )
    scheduler.add_job(func=CleanLogs().start, trigger='cron', hour=1, minute=0, id='clean_logs')
    scheduler.add_job(func=UpdateTemplateInfo().start, trigger='cron', hour=1, minute=0, id='update_template_info')

    scheduler.start()
